package code.google.jcustomize;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.*;

import javax.servlet.jsp.el.ELException;
import javax.servlet.jsp.el.ExpressionEvaluator;
import javax.servlet.jsp.el.FunctionMapper;
import javax.servlet.jsp.el.VariableResolver;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;

import org.apache.commons.el.ExpressionEvaluatorImpl;
import org.apache.xml.utils.PrefixResolver;
import org.apache.xml.utils.PrefixResolverDefault;
import org.apache.xpath.XPathAPI;
import org.apache.xpath.objects.XObject;
import org.w3c.dom.*;

public class XmlConfig extends AbstractConfig {
    private static Method getXmlEncoding = null;
    private boolean USE_FS_PATH     = false;
    private List items = new ArrayList();
    private String path;
    private Map prefixes = new HashMap();

    public boolean patch(ConfigSource cs, VariableResolver variableResolver, FunctionMapper functionMapper) {
        boolean critical = false;
        try {
            InputStream is = cs.fileConfigRead(path, USE_FS_PATH);
            Document doc = cs.readDocument(is);
            is.close();
            ExpressionEvaluator ee = new ExpressionEvaluatorImpl();
            
            for (Iterator it = items.iterator(); it.hasNext(); ) {
                ConfigItem item = (ConfigItem)it.next();
                if (item.getCondition() != null) {
                    Boolean cond = (Boolean) ee.evaluate(item.getCondition(), Boolean.class, variableResolver, functionMapper);
                    if (cond != null && !cond.booleanValue()) continue;
                }
                Node target = null;
                try {
                    XPathAPI.selectSingleNode(doc, item.getPath(), doc.getDocumentElement());
                } catch (TransformerException te) {
                    //System.err.println(te.getMessage());
                }
                if (target == null)
                    target = selectSingleNode(doc, item.getPath());
                if (target == null)
                    if (item.isRequired())
                        throw new RuntimeException("XML-Config '"+getName()+"': required item '"+item.getPath()+"' not found");
                    else continue;

                if (item.getAction() == ConfigItem.ACTION_REMOVE) {
                    target.getParentNode().removeChild(target);
                    continue;
                }

                String value = item.getValue();
                for (Iterator itv = item.getConditionVals().iterator(), itt = item.getConditionTests().iterator(); itv.hasNext() && itt.hasNext(); ) {
                    String v = (String)itv.next();
                    String t = (String)itt.next();
                    Boolean cond = (Boolean) ee.evaluate(t, Boolean.class, variableResolver, functionMapper);
                    if (cond != null && cond.booleanValue()) {
                        value = v; break;
                    }
                }
                if (value.indexOf("${") >= 0) {
                    value = (String)ee.evaluate(value, String.class, variableResolver, functionMapper);
                    if (value== null && item.isRequired()) throw new RuntimeException("Can't evaluate required value for '"+item.getPath()+"', config '"+getName()+"'");
                }
                
                if (item.getAction() == ConfigItem.ACTION_CHANGE) {
                    if (target.getNodeType() == Node.TEXT_NODE || target.getNodeType() == Node.ATTRIBUTE_NODE) {
                        if (item.getReplace() != null) {
                            String current = target.getNodeValue();
                            value = (current != null) ? current.replaceAll(item.getReplace(), value) : null;
                        }
                        target.setNodeValue(value);
                    }
                    else if (target.getNodeType() == Node.ELEMENT_NODE) {
                        if (item.getReplace() == null) {
                            for (Node child = target.getFirstChild(); child != null; ) {
                                Node next = child.getNextSibling();
                                if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE)
                                    target.removeChild(child);
                                child = next;
                            }
                            if (value != null && (value.indexOf('>') >= 0 || value.indexOf('<') >= 0 || value.indexOf('&') >= 0))
                                target.appendChild(doc.createCDATASection(value));
                            else
                                target.appendChild(doc.createTextNode(value));
                        } else {
                            for (Node child = target.getFirstChild(); child != null; child = child.getNextSibling()) {
                                if (child.getNodeType() != Node.TEXT_NODE)
                                    continue;
                                String current = child.getNodeValue();
                                if (current == null) continue; 
                                child.setNodeValue(current.replaceAll(item.getReplace(), value));
                            }
                        }
                    }
                }
            }
            
            critical = true;
            
            OutputStream os = cs.fileConfigWrite(path, false, USE_FS_PATH);
            serialize(doc, os, false, true);
            os.close();
            return true;
        } catch (IOException ioe) {
            if (!critical) {
                ioe.printStackTrace();
                return false;
            }
            throw new RuntimeException(ioe);
        } catch (TransformerException te) {
            throw new RuntimeException(te);
        } catch (ELException ele) {
            throw new RuntimeException(ele);
        }
    }

    protected void readItems(Node config) {
        Element cfg = (Element)config;
        path = cfg.getAttribute("path");
        if ("".equals(path))
            throw new IllegalArgumentException("Path is required for xml-config '"+getName()+"'");
        
        for (Node child = config.getFirstChild(); child != null; child = child.getNextSibling()) {
            if (child.getNodeType() != Node.ELEMENT_NODE) continue;
            if ("item".equals(child.getNodeName())) {
                ConfigItem item = readItem((Element)child);
                if (item != null) items.add(item);
            } 
            else if ("xml-prefix".equals(child.getNodeName())) {
                Element pfx = (Element)child;
                prefixes.put(pfx.getAttribute("name"), pfx.getAttribute("ns"));
            }
        }
    }
    
    private ConfigItem readItem(Element el) {
        ConfigItem item = new ConfigItem();
        for (Node child = el.getFirstChild(); child != null; child = child.getNextSibling()) {
            if (child.getNodeType() != Node.ELEMENT_NODE) continue;
            final String name = child.getNodeName();
            if ("xpath".equals(name))
                item.setPath(getElementValue(child));
            else if ("value".equals(name)) {
                String cond = ((Element)child).getAttribute("when");
                if (cond.length() > 0)
                    item.addConditionValue(getElementValue(child), cond);
                else
                    item.setValue(getElementValue(child));
                cond = ((Element)child).getAttribute("replace");
                if (cond.length() > 0) item.setReplace(cond);
            }
            else if ("action".equals(name))
                item.setAction(getElementValue(child));
            else if ("condition".equals(name))
                item.setCondition(getElementValue(child));
        }
        if (item.getPath() == null) return null;
        String req = el.getAttribute("required").toLowerCase();
        item.setRequired("true".equals(req) || "yes".equals(req));
        if (item.getAction() != ConfigItem.ACTION_REMOVE && item.getValue() == null) return null;
        return item;
    }

    public boolean skip(VariableResolver variableResolver, FunctionMapper functionMapper) {
        if (super.skip(variableResolver, functionMapper)) return true;
        return items.isEmpty();
    }
    
    private Node selectSingleNode(final Node contextNode, String xpath) throws TransformerException {
        try {
            XObject ret = XPathAPI.eval(contextNode, xpath, new PrefixResolver() {
                PrefixResolver defaultResolver = new PrefixResolverDefault(contextNode);

                String namespaceForPrefix(String prefix) {
                    if (prefixes.containsKey(prefix))
                        return (String)prefixes.get(prefix);
                    if ("".equals(prefix)) return "";
                    return null;
                }

                public String getNamespaceForPrefix(String prefix) {
                    String ret = namespaceForPrefix(prefix);
                    return ret != null ? ret : defaultResolver
                            .getNamespaceForPrefix(prefix);
                }

                public String getNamespaceForPrefix(String prefix, Node context) {
                    String ret = namespaceForPrefix(prefix);
                    return ret != null ? ret : defaultResolver
                            .getNamespaceForPrefix(prefix, context);
                }

                public String getBaseIdentifier() {
                    String base = defaultResolver.getBaseIdentifier();
                    return base;
                }

                public boolean handlesNullPrefixes() {
                    return false;
                }
            });
            Node result = ret == null ? null : ret.nodeset().nextNode();
            return result;
        } catch (TransformerException te) {
            throw te;
        }
    }

    public static void serialize(Document doc, OutputStream os, boolean indent, boolean serializeAll) {
        TransformerFactory tf = TransformerFactory.newInstance();
        try {
            Transformer t = tf.newTransformer();
            t.setOutputProperty(OutputKeys.INDENT, indent ? "yes" : "no");
            if (serializeAll) {
                try {
                    String originalEncoding = getXmlEncoding != null ? (String)getXmlEncoding.invoke(doc, (Object[])null) : null;
                    if (originalEncoding != null)
                        t.setOutputProperty(OutputKeys.ENCODING, originalEncoding);
                } catch (NoSuchMethodError nsmer) {
                    // old jdk, ignore
                }
                for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) {
                    if (child.getNodeType() != Node.DOCUMENT_TYPE_NODE) continue;
                    DocumentType dt = (DocumentType)child;
                    String pub = dt.getPublicId();
                    String sys = dt.getSystemId();
                    if (pub != null && sys != null) {
                        t.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, pub);
                        t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, sys);
                    }
                    break;
                }
            }
            Source source = new DOMSource(doc);
            Result result = new StreamResult(os);
            t.transform(source, result);
        } catch (Exception e) {}
    }
    
    static {
        try {
            getXmlEncoding = Document.class.getDeclaredMethod("getXmlEncoding", (Class[])null);
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
        }
    }

}
